package com.wispx.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.wispx.shortlink.project.common.convention.exception.ClientException;
import com.wispx.shortlink.project.common.convention.exception.ServiceException;
import com.wispx.shortlink.project.configuration.GotoDomainWhiteListConfiguration;
import com.wispx.shortlink.project.dao.entity.*;
import com.wispx.shortlink.project.dao.mapper.*;
import com.wispx.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.wispx.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.wispx.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.wispx.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.wispx.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.wispx.shortlink.project.dto.resp.*;
import com.wispx.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.wispx.shortlink.project.service.LinkStatsTodayService;
import com.wispx.shortlink.project.service.ShortLinkGeneratorService;
import com.wispx.shortlink.project.service.ShortLinkService;
import com.wispx.shortlink.project.toolkit.HashUtil;
import com.wispx.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.wispx.shortlink.project.common.constant.RedisKeyConstant.*;
import static com.wispx.shortlink.project.common.enums.ValidDateTypeEnum.PERMANENT;
import static com.wispx.shortlink.project.toolkit.HashUtil.convertDecToBase62;

/**
 * 短链接服务实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final ShortLinkGeneratorService shortLinkGeneratorService;
    private final LinkStatsTodayService linkStatsTodayService;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;


    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    private String generateSuffix(ShortLinkCreateReqDTO requestParam){
        int generateCount = 0;
        String domain = createShortLinkDefaultDomain;
        String suffix = null;
        while(true){
            if(generateCount > 10){
                throw new ClientException("重复生成短链接请稍后再试。。。");
            }
            suffix = HashUtil.hashToBase62(requestParam.getOriginUrl() + UUID.randomUUID().toString());
            String fullShortLink = domain + "/" + suffix;
            if (!shortLinkCreateCachePenetrationBloomFilter.contains(fullShortLink)) {
                break;
            }
            generateCount ++;
        }
        return suffix;
    }


    /**
     * 自增实现生成短链接
     */
    private String generateSuffixV2(ShortLinkCreateReqDTO requestParam){
        long id = shortLinkGeneratorService.genID();
        String shortLinkID = convertDecToBase62(id);
        System.out.println(shortLinkID);
        return shortLinkID;
    }

    private final Cache<String, String> urlCache = Caffeine.newBuilder()
                                                    .expireAfterWrite(10, TimeUnit.MINUTES)
                                                    .build();

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        verificationWhiteList(requestParam.getOriginUrl());
        String suffix = urlCache.getIfPresent(requestParam.getOriginUrl());
        if (!StrUtil.isNotBlank(suffix)){
            suffix = generateSuffix(requestParam);
            urlCache.put(requestParam.getOriginUrl(),suffix);
        }
        String fullShortLink = StrBuilder.create(createShortLinkDefaultDomain)
                .append('/')
                .append(suffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .delTime(0L)
                .domain(createShortLinkDefaultDomain)
                .shortUri(suffix)
                .fullShortUrl(fullShortLink)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .enableStatus(0)
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .build();
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortLink)
                .build();

        try {
            // 高并发下，可能抛出数据库唯一索引异常
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        }catch (DuplicateKeyException ex){
            log.warn("短链接{}重复入库",fullShortLink);
            throw new ServiceException("短链接重复生成");
        }

        // 新增时添加到缓存进行预热
        stringRedisTemplate.opsForValue().set(GOTO_SHORT_LINK_KEY + fullShortLink,shortLinkDO.getOriginUrl(),
                LinkUtil.getValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS);
        shortLinkCreateCachePenetrationBloomFilter.add(fullShortLink);

        return ShortLinkCreateRespDTO.builder()
                .originUrl(shortLinkDO.getOriginUrl())
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .gid(requestParam.getGid())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkPageRespDTO> result = baseMapper.pageShortLink(requestParam);
        return result.convert(each -> {
            ShortLinkPageRespDTO bean = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            bean.setFullShortUrl("http://" + bean.getFullShortUrl());
            return bean;
        });
    }

    @Override
    public List<ShortLinkCountQueryRespDTO> listShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid", "count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .eq("enable_status",0)
                .groupBy("gid");
        List<Map<String,Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList,ShortLinkCountQueryRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        verificationWhiteList(requestParam.getOriginUrl());
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginalGid())
                .eq(ShortLinkDO::getFullShortUrl,requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if(hasShortLinkDO == null){
            throw new ClientException("该记录不存在");
        }

        // gid相同，没有修改分组
        if(Objects.equals(requestParam.getOriginalGid(),requestParam.getGid())){
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                    .originUrl(requestParam.getOriginUrl())
                    .clickNum(hasShortLinkDO.getClickNum())
                    .gid(requestParam.getGid())
                    .favicon(hasShortLinkDO.getFavicon())
                    .enableStatus(hasShortLinkDO.getEnableStatus())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .build();
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(),PERMANENT.getType()),ShortLinkDO::getValidDate,null);
            baseMapper.update(shortLinkDO,updateWrapper);
        }else{
            // gid不同，先根据originalGid删除，再根据gid新增

            // 锁fullShortUrl，一个短链接同一时刻只允许一个修改
            RReadWriteLock rwLock = redissonClient.getReadWriteLock(LOCK_GID_UPDATE_KEY + requestParam.getFullShortUrl());
            // 分布式写锁
            RLock rLock = rwLock.writeLock();
            // 使用了消息队列后，消费者线程就固定了
            // 总共就 1个拿写锁的线程 和 消费者线程取去竞争，这种阻塞是可以接受的 (1 + 10)
            rLock.lock();

            /*
            if(!rLock.tryLock()){
                // 没有得到写锁的线程,直接返回
                throw new ServiceException("短链接正在被访问，请稍后再试...");
            }*/

            try {
                // 将短链接相关的表进行数据修改，如果涉及到分片行为，先删除原有数据再新增。如果不涉及分片行为，只需要修改对应的数据库表记录即可。

                // 删除t_link相关数据
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);
                // 新增t_link相关数据
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        //.favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);

                // 修改t_link_stats_today相关数据
                LambdaQueryWrapper<LinkStatsTodayDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkStatsTodayDO.class)
                        .eq(LinkStatsTodayDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkStatsTodayDO::getGid, hasShortLinkDO.getGid());
                        //.eq(LinkStatsTodayDO::getDelFlag, 0);
                List<LinkStatsTodayDO> linkStatsTodayDOList = linkStatsTodayMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    linkStatsTodayMapper.deleteBatchIds(linkStatsTodayDOList.stream()
                            .map(LinkStatsTodayDO::getId)
                            .toList()
                    );
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid()));
                    linkStatsTodayService.saveBatch(linkStatsTodayDOList);
                }

                // 修改t_link_goto相关数据
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);

                // 修改t_link_access_stats相关数据
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);

                // 修改t_link_locale_stats相关数据
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);

                // 修改t_link_os_stats相关数据
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);

                // 修改t_link_browser_stats相关数据
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);

                // 修改t_link_device_stats相关数据
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);

                // 修改t_link_network_stats相关数据
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);

                // 修改t_link_access_logs相关数据
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);

            } finally {
                rLock.unlock();
            }
        }

        // 有效期类型不同 || 有效期与之前记录不一致
        if(!Objects.equals(hasShortLinkDO.getValidDateType(),requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(),requestParam.getValidDate())){
            // 删除缓存中的记录
            stringRedisTemplate.delete(GOTO_SHORT_LINK_KEY + requestParam.getFullShortUrl());
            // 不是永久有效 && 有效期过期
            if(hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date())){
                // 最新有效期类型为永久有效 || 有效期不失效
                if(Objects.equals(requestParam.getValidDateType(),PERMANENT.getType()) || requestParam.getValidDate().after(new Date())){
                    // 删除缓存中的'空值'
                    stringRedisTemplate.delete(IS_NULL_GOTO_SHORT_LINK + requestParam.getFullShortUrl());
                }
            }
        }
    }

    @SneakyThrows
    @Override
    public void restoreLink(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String serverName = request.getServerName();
        // 服务部署在80端口就省略，不是就加上端口
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(80, each))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        // localhost/1194aO 不包括协议
        String fullShortUrl = serverName + serverPort + "/" + shortUri;

        // 查询RedisCache
        String originalUrl = stringRedisTemplate.opsForValue().get(GOTO_SHORT_LINK_KEY + fullShortUrl);
        if(StrUtil.isNotBlank(originalUrl)){
            response.sendRedirect(originalUrl);
            // 短链接监控
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, null, statsRecord);
            return;
        }
        if (!shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
            response.sendRedirect("/page/notfound");
            return;
        }

        // 是否缓存空值
        String isNull = stringRedisTemplate.opsForValue().get(IS_NULL_GOTO_SHORT_LINK + fullShortUrl);
        if(StrUtil.isNotBlank(isNull)){
            response.sendRedirect("/page/notfound");
            return;
        }

        RLock lock = redissonClient.getLock(LOCK_GOTO_SHORT_LINK + fullShortUrl);
        lock.lock();
        try{
            // double-checked locking
            originalUrl = stringRedisTemplate.opsForValue().get(GOTO_SHORT_LINK_KEY + fullShortUrl);
            if(StrUtil.isNotBlank(originalUrl)){
                response.sendRedirect(originalUrl);
                ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, null, statsRecord);
                return;
            }
            isNull = stringRedisTemplate.opsForValue().get(IS_NULL_GOTO_SHORT_LINK + fullShortUrl);
            if(StrUtil.isNotBlank(isNull)){
                response.sendRedirect("/page/notfound");
                return;
            }

            // 根据fullShortUrl在t_link_goto中查找gid
            LambdaQueryWrapper<ShortLinkGotoDO> queryGidWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryGidWrapper);
            if(shortLinkGotoDO == null){
                // 缓存空值，防止缓存穿透
                stringRedisTemplate.opsForValue().set(IS_NULL_GOTO_SHORT_LINK + fullShortUrl, "-",10,TimeUnit.DAYS);
                // 进行风控
                return;
            }

            // 根据gid、fullShortUrl在t_link查找原始链接
            LambdaQueryWrapper<ShortLinkDO> queryOriginalUrlWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus,0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryOriginalUrlWrapper);
            if((shortLinkDO == null ) || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))){
                // 未启用短链接 || 已过有效期 同样缓存空值
                stringRedisTemplate.opsForValue().set(IS_NULL_GOTO_SHORT_LINK + fullShortUrl, "-",10,TimeUnit.DAYS);
                response.sendRedirect("/page/notfound");
                return;
            }

            // 加入到缓存
            stringRedisTemplate.opsForValue().set(GOTO_SHORT_LINK_KEY + fullShortUrl, shortLinkDO.getOriginUrl(),
                    LinkUtil.getValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS);
            response.sendRedirect(originalUrl);
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, shortLinkDO.getGid(), statsRecord);
        }
        finally {
            lock.unlock();
        }
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    // 短链接统计实体
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        // uv标识
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = request.getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        // 添加Cookie任务
        Runnable addResponseCookieTask = () -> {
            // 随机UUID作为用户唯一标识
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            // 在浏览器中保存10天
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            // 设置Cookie关联的路径
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            response.addCookie(uvCookie);
            // Redis中的Set集合
            Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_UV_STATS_KEY + fullShortUrl, uv.get());
            uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
        };

        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_UV_STATS_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }

        String remoteAddr = LinkUtil.getIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_UIP_STATS_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = (uipAdded != null && uipAdded > 0L);
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }

    @Transactional
    @Override
    public void shortLinkStats(String fullShortUrl,String gid,ShortLinkStatsRecordDTO statsRecord){
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("fullShortUrl", fullShortUrl);
        producerMap.put("gid", gid);
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        shortLinkStatsSaveProducer.send(producerMap);
    }

    private void verificationWhiteList(String originalUrl){
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if(enable == null || !enable){
            return;
        }
        String domain = LinkUtil.extractDomain(originalUrl);
        if(StrUtil.isBlank(domain)){
            throw new ClientException("客户端填写跳转的连接有误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if(!details.contains(domain)){
            throw new ClientException("请生成以下跳转链接:" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
